﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using SimulationPresenter.Presentation.Primitives;
using SimulationPresenter.UI;

namespace SimulationPresenter.Presentation.Helpers
{
    static internal class MeshExtensions
    {
        private const int IndexSizeInBytes = 2;

        static internal Mesh LoadFromFile(string fileName, ref Material[] materials, ref Texture[] textures, Device device)
        {
            LogManager.LogInfo("Loading file ({0})", fileName);
            ExtendedMaterial[] materialarray;
            var mesh = Mesh.FromFile(fileName, MeshFlags.Managed, device, out materialarray);

            LogManager.LogInfo("Mesh loaded from file. Found {0} materials.",
                null == materialarray ? 0 : materialarray.Length);
            
            if ((materialarray != null) && (materialarray.Length > 0))
            {
                materials = new Material[materialarray.Length];
                textures = new Texture[materialarray.Length];

                for (int i = 0; i < materialarray.Length; i++)
                {
                    materials[i] = materialarray[i].Material3D;
                    materials[i].Ambient = materials[i].Diffuse;

                    if (!string.IsNullOrEmpty(materialarray[i].TextureFilename))
                    {
                        //Combine directory path of a mesh with texture path
                        var fileInfo = new FileInfo(fileName);
                        var directoryInfo = fileInfo.Directory;
                        string texturePath = Path.Combine(directoryInfo.FullName, materialarray[i].TextureFilename);
                        LogManager.LogInfo("Trying to load texture from file {0}.",
                                            texturePath);
                        textures[i] = TextureLoader.FromFile(device, texturePath);
                    }
                }
            }

            LogManager.LogInfo("Cloning mesh");
            mesh = mesh.Clone(mesh.Options.Value, CustomVertex.PositionNormalTextured.Format, device);

            LogManager.LogInfo("Computing mesh normals");
            mesh.ComputeNormals();

            LogManager.LogInfo("Mesh done");
            return mesh;
        }

        #region Mesh spliter
        /// <summary>
        /// Splits mesh into multiple meshes, each representing one subset.
        /// </summary>
        /// <param name="mesh"></param>
        /// <param name="device"></param>
        /// <returns></returns>
        static internal MeshShape[] SplitMesh(this Mesh mesh, Device device, Material[] materials, Texture[] textures)
        {
            LogManager.LogInfo("Starting Mesh Split");
            IList<MeshShape> meshes = new List<MeshShape>();

            #region Optimize mesh
            LogManager.LogInfo("Optimizing Mesh");
            var adjency = new int[mesh.NumberFaces * 3];
            mesh.GenerateAdjacency(0.01f, adjency);
            mesh = mesh.Optimize(MeshFlags.OptimizeAttributeSort, adjency);
            LogManager.LogInfo("Optimizing Mesh done");
            #endregion

            var attributeRanges = mesh.GetAttributeTable();
            var attributeCount = attributeRanges.Length;

            //For each "subset" create a mesh and add it to list
            LogManager.LogInfo("Looping through {0} subsets",
                                    attributeCount);
            for (var i = 0; i < attributeCount; i++)
            {
                LogManager.LogInfo("Retrieving subset {0} of {1}",
                                        i,
                                        attributeCount);
                LogManager.LogInfo("Material name is {0}");
                var indexArray = mesh.GetSubsetIndices(attributeRanges[i]);
                
                LogManager.LogInfo("Found {0} indices",
                                        indexArray.Length);

                LogManager.LogInfo("Building new Mesh");
                var meshBase = mesh.IndicesToMesh(indexArray, device);
                LogManager.LogInfo("New mesh built");

                meshBase.Material = materials[attributeRanges[i].AttributeId];
                meshBase.Texture = textures[attributeRanges[i].AttributeId];

                meshes.Add(meshBase);
                LogManager.LogInfo("Finished mesh {0} of {1}",
                        i,
                        attributeCount);
            }

            LogManager.LogInfo("Spliting Mesh done");
            return meshes.ToArray();
        }

        /// <summary>
        /// Gets index buffers of all subsets in mesh
        /// </summary>
        /// <param name="mesh"></param>
        /// <param name="range"></param>
        /// <returns></returns>
        static private short[] GetSubsetIndices(this BaseMesh mesh, AttributeRange range)
        {
            var indexStart = range.FaceStart * 3;
            var indexCount = range.FaceCount*3;

            //Get graphic stream
            using (var meshStream = mesh.IndexBuffer.Lock(indexStart * IndexSizeInBytes, indexCount * 2, LockFlags.ReadOnly))
            {
                return meshStream.ToArray<short, short>(x => x, indexCount);
            }
        }

        /// <summary>
        /// Converts subset given by indices array into new mesh
        /// </summary>
        /// <param name="mesh"></param>
        /// <param name="indices"></param>
        /// <param name="device"></param>
        /// <returns></returns>
        static internal MeshShape IndicesToMesh(this Mesh mesh, short[] indices, Device device)
        {
            var vertices = mesh.GetVertices();
            var meshVertices = vertices.Where((x, i) => indices.Contains((short)i)).ToArray();
            var indicesReorganized = UpdateIndices(indices.ToArray(), vertices, meshVertices, (x, y) => x.Position == y.Position);

            return new MeshShape
                       {
                           Vertices = meshVertices,
                           Indices = indicesReorganized
                       };
        }
        #endregion

        #region Indices updater
        //Todo: Export IndicesUpdater as external class. Might be an extension for T[]
        public delegate bool ElementComparer<T>(T element1, T element2);

        /// <summary>
        /// Udpates indices array with values of index in new array
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="oldIndices"></param>
        /// <param name="oldValues"></param>
        /// <param name="newValues"></param>
        /// <param name="comparer"></param>
        /// <returns></returns>
        private static short[] UpdateIndices<T>(short[] oldIndices, T[] oldValues, T[] newValues, ElementComparer<T> comparer)
        {
            int iterationsCount = oldIndices.Length;
            var newIndices = new short[iterationsCount];

            for (int i = 0; i < iterationsCount; i++)
            {
                newIndices[i] = GetNewIndex(oldValues[oldIndices[i]], newValues, comparer);
            }

            return newIndices;
        }

        /// <summary>
        /// Returns updated index of given element in newly built array
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="lookUpElement"></param>
        /// <param name="values"></param>
        /// <param name="comparer"></param>
        /// <returns></returns>
        private static short GetNewIndex<T>(T lookUpElement, T[] values, ElementComparer<T> comparer)
        {
            for (short i = 0; i < values.Length; i++)
            {
                if (comparer(values[i], lookUpElement))
                {
                    return i;
                }
            }
            throw new NotFoundException("Old index was not found in the list");
        }
        #endregion

        /// <summary>
        /// Gets array of vertices from a mesh. Vertex format is Vector3
        /// </summary>
        /// <param name="mesh"></param>
        /// <returns></returns>
        static internal CustomVertex.PositionNormalTextured[] GetVertices(this Mesh mesh)
        {
            var verticesStream = mesh.VertexBuffer.Lock(0, 0, LockFlags.None);
            var vertices = verticesStream.ToArray<CustomVertex.PositionNormalTextured, CustomVertex.PositionNormalTextured>(x => x, mesh.NumberVertices);
            mesh.VertexBuffer.Unlock();
            return vertices;
        }

        #region Position updater
        public delegate void PositionUpdater<T>(ref T vertex, Vector3 vector);
        /// <summary
        /// Updates postion of vertices based on given vectors
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="vertices"></param>
        /// <param name="vectors"></param>
        /// <param name="positionUpdater"></param>
        public static void UpdatePositions<T>(T[] vertices, Vector3[] vectors, PositionUpdater<T> positionUpdater)
        {
            if (vertices.Count() != vectors.Count())
            {
                throw new ArgumentException("vectors", "Set of vertexes does no match vertices in case of amount.");
            }

            for (int i = 0; i < vertices.Length; i++)
            {
                positionUpdater(ref vertices[i], vectors[i]);
            }
        } 
        #endregion
    }

    #region Not working since DX failure
    ///// <summary>
    ///// Gets list of indices arrays, each for one stripe of subset represented by subset Id
    ///// </summary>
    ///// <param name="mesh"></param>
    ///// <param name="subsetId"></param>
    ///// <param name="device"></param>
    ///// <returns></returns>
    //static private IList<short[]> GetSubsetIndexBuffers(this BaseMesh mesh, int subsetId)
    //{
    //    var listOfIndices = new List<short[]>();
    //    int numberOfIndicesAll;
    //    GraphicsStream graphicsStream;
    //    int numberOfStripes;

    //    var indexBuffer = Mesh.ConvertMeshSubsetToStrips(mesh,
    //                                                    subsetId,
    //                                                    MeshFlags.Managed,
    //                                                    out numberOfIndicesAll,
    //                                                    out graphicsStream,
    //                                                    out numberOfStripes);

    //    int[] indicesNumbers = graphicsStream.ToArray<Int32, int>(x => x, numberOfStripes);

    //    var indexStream = indexBuffer.Lock(0, 0, LockFlags.None);
    //    var indicesAll = indexStream.ToArray<short, short>(x => x, numberOfIndicesAll);
    //    indexBuffer.Unlock();

    //    int offset = 0;
    //    foreach (var numberOfIndices in indicesNumbers)
    //    {
    //        var indices = new short[numberOfIndices];
    //        int index = 0;

    //        for (; index < numberOfIndices; index++)
    //        {
    //            indices[index] = indicesAll[index + offset];
    //        }

    //        offset += index;

    //        listOfIndices.Add(indices);
    //    }


    //    return listOfIndices;
    //}
    #endregion

    #region Not used anymore
    ///// <summary>
    ///// Builds mesh based on vertices and indices array
    ///// </summary>
    ///// <typeparam name="TVertex"></typeparam>
    ///// <param name="vertices"></param>
    ///// <param name="indices"></param>
    ///// <param name="device"></param>
    ///// <returns></returns>
    //internal static Mesh BuildMesh<TVertex>(TVertex[] vertices, short[] indices, Device device)
    //{
    //    #region Check and get Vertex Format
    //    var format = typeof(TVertex).GetField("Format");

    //    if (null == format)
    //    {
    //        throw new ArgumentException("Type of vertices array is not a CustomVertex. Element is null.");
    //    }

    //    var vertexFormat = format.GetValue(typeof(TVertex).MakeByRefType());
    //    if (!(vertexFormat is VertexFormats))
    //    {
    //        throw new ArgumentException("Type of vertices array is not a CustomVertex. Element can't be casted to VertexFormats");
    //    }
    //    #endregion

    //    var meshVertexFormat = (VertexFormats)vertexFormat;
    //    if (0 != indices.Length % 3)
    //    {
    //        throw new ArgumentException("Indices number should be divideable by 3!", "indices");
    //    }

    //    var mesh = new Mesh(indices.Length / 3, vertices.Length, MeshFlags.Managed, meshVertexFormat, device);

    //    mesh.SetIndexBufferData(indices, LockFlags.None);
    //    mesh.SetVertexBufferData(vertices, LockFlags.None);

    //    var adjency = new int[indices.Length];
    //    mesh.GenerateAdjacency(0.5f, adjency);
    //    mesh.OptimizeInPlace(MeshFlags.OptimizeVertexCache, adjency);

    //    return mesh;
    //} 
    #endregion
}
